package com.unismc.supervisor;

import org.apache.xmlrpc.XmlRpcException;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.MalformedURLException;
import java.net.URL;
import java.util.Map;

/**
 * Implement All the methods of Supervisor XML-RPC interface
 *
 * @author Ben Wen
 * @see <a href="http://www.supervisord.org/api.html">http://www.supervisord.org/api.html</a>
 */
public class Supervisor {
    private final static Logger LOGGER = LoggerFactory.getLogger(Supervisor.class);

    private final static String DEFAULT_URL = "http://localhost:9001/RPC2";
    private final static String DEFAULT_NAMESPACE = "supervisor";
    private final static String SYSTEM_NAMESPACE = "system";
    private final static String DOT = ".";

    private String url = DEFAULT_URL;
    private String username;
    private String password;

    private XmlRpcClient client;

    private Supervisor() {
    }

    public static Supervisor url() {
        Supervisor me = new Supervisor();
        return me;
    }

    public static Supervisor url(String apiUrl) {
        Supervisor me = new Supervisor();
        me.url = apiUrl;
        return me;
    }

    public Supervisor auth(String username, String
            password) {
        this.username = username;
        this.password = password;
        return this;
    }

    public Supervisor configXmlRpcClient() {
        client = new XmlRpcClient();
        XmlRpcClientConfigImpl config = new XmlRpcClientConfigImpl();
        try {
            config.setServerURL(new URL(url));
        } catch (MalformedURLException e) {
            e.printStackTrace();
        }
        config.setEnabledForExtensions(true);
        config.setConnectionTimeout(5 * 1000);
        config.setReplyTimeout(50 * 1000);
        if (null != username) {
            config.setBasicUserName(username);
            config.setBasicPassword(password);
        }
        client.setConfig(config);
        return this;
    }

    /**
     * build a full method call with namespace prefix
     *
     * @param method method name
     * @return rpc
     */
    private String buildFullMethodCall(final String method) {
        StringBuilder stringBuilder = new StringBuilder();
        return stringBuilder.append(DEFAULT_NAMESPACE).append(DOT).append(method).toString();
    }

    /**
     * build a full method call with namespace prefix
     *
     * @param namespace namespace
     * @param method    method name
     * @return rpc
     */
    private String buildFullMethodCall(final String namespace, final String method) {
        StringBuilder stringBuilder = new StringBuilder();
        return stringBuilder.append(namespace).append(DOT).append(method).toString();
    }

    /**
     * Return the version of the RPC API used by supervisord
     *
     * @return string version version id
     * @throws XmlRpcException
     */
    public String getAPIVersion() throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._GET_API_VERSION), new Object[]{});
    }

    /**
     * Return the version of the supervisor package in use by supervisord
     *
     * @return string version version id
     * @throws XmlRpcException
     */
    public String getSupervisorVersion() throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._GET_SUPERVISOR_VERSION), new Object[]{});
    }

    /**
     * Return identifying string of supervisord
     *
     * @return string identifier identifying string
     * @throws XmlRpcException
     */
    public String getIdentification() throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._GET_IDENTIFICATION), new Object[]{});
    }

    /**
     * Return current state of supervisord as a struct
     *
     * @return struct A struct with keys int statecode, string statename
     * @throws XmlRpcException
     */
    public Map<String, Object> getState() throws XmlRpcException {
        return (Map<String, Object>) client.execute(buildFullMethodCall(Methods._GET_STATE), new Object[]{});
    }

    /**
     * Return the PID of supervisord
     *
     * @return int PID
     * @throws XmlRpcException
     */
    public int getPID() throws XmlRpcException {
        return (Integer) client.execute(buildFullMethodCall(Methods._GET_PID), new Object[]{});
    }

    /**
     * Read length bytes from the main log starting at offset
     *
     * @param offset offset to start reading from
     * @param length number of bytes to read from the log.
     * @return string result Bytes of log
     * @throws XmlRpcException
     */
    public String readLog(int offset, int length) throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._READ_LOG), new Object[]{offset, length});
    }

    /**
     * Clear the main log.
     *
     * @return boolean result always returns True unless error
     * @throws XmlRpcException
     */
    public boolean clearLog() throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._CLEAR_LOG), new Object[]{});
    }

    /**
     * Clear the main log.
     *
     * @return boolean result always returns True unless error
     * @throws XmlRpcException
     */
    public boolean shutdown() throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._SHUTDOWN), new Object[]{});
    }

    /**
     * Restart the supervisor process
     *
     * @return boolean result always return True unless error
     * @throws XmlRpcException
     */
    public boolean restart() throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._RESTART), new Object[]{});
    }

    /**
     * Get info about a process named name
     *
     * @param name The name of the process (or ‘group:name’)
     * @return struct result A structure containing data about the process
     * @throws XmlRpcException
     */
    public Object getProcessInfo(String name) throws XmlRpcException {
        return (Object) client.execute(buildFullMethodCall(Methods._GET_PROCESS_INFO), new Object[]{name});
    }

    /**
     * Get info about all processes
     *
     * @return array result An array of process status results
     * @throws XmlRpcException
     */
    public Object[] getAllProcessInfo() throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._GET_ALL_PROCESS_INFO), new Object[]{});
    }

    /**
     * Start a process
     *
     * @param name Process name (or group:name, or group:*)
     * @param wait Wait for process to be fully started
     * @return result Always true unless error
     * @throws XmlRpcException
     */
    public boolean startProcess(String name, boolean wait) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._START_PROCESS), new Object[]{name, wait});
    }

    /**
     * Start all processes listed in the configuration file
     *
     * @param wait Wait for each process to be fully started
     * @return array result An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] startAllProcesses(boolean wait) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._START_ALL_PROCESSES), new Object[]{wait});
    }

    /**
     * Start all processes in the group named ‘name’
     *
     * @param name The group name
     * @param wait Wait for each process to be fully started
     * @return array result An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] startProcessGroup(String name, boolean wait) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._START_PROCESS_GROUP), new Object[]{name, wait});
    }

    /**
     * Stop a process named by name
     *
     * @param name The name of the process to stop (or ‘group:name’)
     * @param wait Wait for the process to be fully stopped
     * @return boolean result Always return True unless error
     * @throws XmlRpcException
     */
    public boolean stopProcess(String name, boolean wait) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._STOP_PROCESS), new Object[]{name, wait});
    }

    /**
     * Start all processes in the group named ‘name’
     *
     * @param name The group name
     * @param wait Wait for each process to be fully started
     * @return array result An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] stopProcessGroup(String name, boolean wait) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._STOP_PROCESS_GROUP), new Object[]{name, wait});
    }

    /**
     * Stop all processes in the process list
     *
     * @param wait Wait for each process to be fully stopped
     * @return array result An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] stopAllProcesses(boolean wait) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._STOP_ALL_PROCESSES), new Object[]{wait});
    }

    /**
     * Send an arbitrary UNIX signal to the process named by name
     *
     * @param name   Name of the process to signal (or ‘group:name’)
     * @param signal Signal to send, as name (‘HUP’) or number (‘1’)
     * @return boolean
     * @throws XmlRpcException
     */
    public boolean signalProcess(String name, String signal) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._SIGNAL_PROCESS), new Object[]{name, signal});
    }

    /**
     * Send a signal to all processes in the group named ‘name’
     *
     * @param name   The group name
     * @param signal Signal to send, as name (‘HUP’) or number (‘1’)
     * @return array
     * @throws XmlRpcException
     */
    public Object[] signalProcessGroup(String name, String signal) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._SIGNAL_PROCESS_GROUP), new Object[]{name, signal});
    }

    /**
     * Send a signal to all processes in the process list
     *
     * @param signal Signal to send, as name (‘HUP’) or number (‘1’)
     * @return array An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] signalAllProcesses(String signal) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._SIGNAL_ALL_PROCESSES), new Object[]{signal});
    }

    /**
     * Send a string of chars to the stdin of the process name. If non-7-bit data is sent (unicode), it is encoded to utf-8 before being sent to the process’ stdin. If chars is not a string or is not unicode, raise INCORRECT_PARAMETERS. If the process is not running, raise NOT_RUNNING. If the process’ stdin cannot accept input (e.g. it was closed by the child process), raise NO_FILE.
     *
     * @param name  The process name to send to (or ‘group:name’)
     * @param chars The character data to send to the process
     * @return boolean result Always return True unless error
     * @throws XmlRpcException
     */
    public boolean sendProcessStdin(String name, String chars) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._SEND_PROCESS_STDIN), new Object[]{name, chars});
    }

    /**
     * Send an event that will be received by event listener subprocesses subscribing to the RemoteCommunicationEvent.
     *
     * @param type String for the “type” key in the event header
     * @param data Data for the event body
     * @return boolean Always return True unless error
     * @throws XmlRpcException
     */
    public boolean sendRemoteCommEvent(String type, String data) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._SEND_REMOTE_COMM_EVENT), new Object[]{type, data});
    }

    /**
     * Reload the configuration.
     * <p>
     * The result contains three arrays containing names of process groups:
     * <p>
     * added gives the process groups that have been added
     * changed gives the process groups whose contents have changed
     * removed gives the process groups that are no longer in the configuration
     *
     * @return array result [[added, changed, removed]]
     * @throws XmlRpcException
     */
    public Object[] reloadConfig() throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._RELOAD_CONFIG), new Object[]{});
    }

    /**
     * Update the config for a running process from config file.
     *
     * @param name name of process group to add
     * @return boolean result true if successful
     * @throws XmlRpcException
     */
    public boolean addProcessGroup(String name) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._ADD_PROCESS_GROUP), new Object[]{name});
    }

    /**
     * Remove a stopped process from the active configuration.
     *
     * @param name name of process group to remove
     * @return boolean result Indicates whether the removal was successful
     * @throws XmlRpcException
     */
    public boolean removeProcessGroup(String name) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._REMOVE_PROCESS_GROUP), new Object[]{name});
    }

    /**
     * Read length bytes from name’s stdout log starting at offset
     *
     * @param name   the name of the process (or ‘group:name’)
     * @param offset offset to start reading from.
     * @param length number of bytes to read from the log.
     * @return string result Bytes of log
     * @throws XmlRpcException
     */
    public String readProcessStdoutLog(String name, int offset, int length) throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._READ_PROCESS_STDOUT_LOG), new Object[]{name, offset, length});
    }

    /**
     * Read length bytes from name’s stderr log starting at offset
     *
     * @param name   the name of the process (or ‘group:name’)
     * @param offset offset to start reading from.
     * @param length number of bytes to read from the log.
     * @return string result Bytes of log
     * @throws XmlRpcException
     */
    public String readProcessStderrLog(String name, int offset, int length) throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(Methods._READ_PROCESS_STDERR_LOG), new Object[]{name, offset, length});
    }

    /**
     * Provides a more efficient way to tail the (stdout) log than readProcessStdoutLog(). Use readProcessStdoutLog() to read chunks and tailProcessStdoutLog() to tail.
     * <p>
     * Requests (length) bytes from the (name)’s log, starting at (offset). If the total log size is greater than (offset + length), the overflow flag is set and the (offset) is automatically increased to position the buffer at the end of the log. If less than (length) bytes are available, the maximum number of available bytes will be returned. (offset) returned is always the last offset in the log +1.
     *
     * @param name   the name of the process (or ‘group:name’)
     * @param offset offset to start reading from
     * @param length maximum number of bytes to return
     * @return array result [string bytes, int offset, bool overflow]
     * @throws XmlRpcException
     */
    public Object[] tailProcessStdoutLog(String name, int offset, int length) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._TAIL_PROCESS_STDOUT_LOG), new Object[]{name, offset, length});
    }

    /**
     * Provides a more efficient way to tail the (stderr) log than readProcessStderrLog(). Use readProcessStderrLog() to read chunks and tailProcessStderrLog() to tail.
     * <p>
     * Requests (length) bytes from the (name)’s log, starting at (offset). If the total log size is greater than (offset + length), the overflow flag is set and the (offset) is automatically increased to position the buffer at the end of the log. If less than (length) bytes are available, the maximum number of available bytes will be returned. (offset) returned is always the last offset in the log +1.
     *
     * @param name   the name of the process (or ‘group:name’)
     * @param offset offset to start reading from
     * @param length maximum number of bytes to return
     * @return array result [string bytes, int offset, bool overflow]
     * @throws XmlRpcException
     */
    public Object[] tailProcessStderrLog(String name, int offset, int length) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._TAIL_PROCESS_STDERR_LOG), new Object[]{name, offset, length});
    }

    /**
     * Clear the stdout and stderr logs for the named process and reopen them.
     *
     * @param name The name of the process (or ‘group:name’)
     * @return boolean result Always True unless error
     * @throws XmlRpcException
     */
    public boolean clearProcessLogs(String name) throws XmlRpcException {
        return (Boolean) client.execute(buildFullMethodCall(Methods._CLEAR_PROCESS_LOGS), new Object[]{name});
    }

    /**
     * Clear all process log files
     *
     * @return array result An array of process status info structs
     * @throws XmlRpcException
     */
    public Object[] clearAllProcessLogs() throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(Methods._CLEAR_ALL_PROCESS_LOGS), new Object[]{});
    }

    /**
     * Return an array listing the available method names
     *
     * @return array result An array of method names available (strings).
     * @throws XmlRpcException
     */
    public Object[] listMethods() throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(SYSTEM_NAMESPACE, Methods._LIST_METHODS), new Object[]{});
    }

    /**
     * Return a string showing the method’s documentation
     *
     * @param name The name of the method.
     * @return string result The documentation for the method name.
     * @throws XmlRpcException
     */
    public String methodHelp(String name) throws XmlRpcException {
        return (String) client.execute(buildFullMethodCall(SYSTEM_NAMESPACE, Methods._METHOD_HELP), new Object[]{name});
    }

    /**
     * Return an array describing the method signature in the form [rtype, ptype, ptype...] where rtype is the return data type of the method, and ptypes are the parameter data types that the method accepts in method argument order.
     *
     * @param name The name of the method.
     * @return array result The result.
     * @throws XmlRpcException
     */
    public Object[] methodSignature(String name) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(SYSTEM_NAMESPACE, Methods._METHOD_SIGNATURE), new Object[]{name});
    }

    /**
     * Process an array of calls, and return an array of results.
     * Calls should be structs of the form {‘methodName’: string, ‘params’: array}.
     * Each result will either be a single-item array containing the result value, or a struct of the form {‘faultCode’: int, ‘faultString’: string}.
     * This is useful when you need to make lots of small calls without lots of round trips.
     *
     * @param calls An array of call requests
     * @return array result An array of results
     * @throws XmlRpcException
     */
    public Object[] multicall(Object[] calls) throws XmlRpcException {
        return (Object[]) client.execute(buildFullMethodCall(SYSTEM_NAMESPACE, Methods._MULTICALL), calls);
    }

    /**
     * Used to call the interface extended by user
     *
     * @param namespace  the namespace defined in ini file
     * @param methodName method name
     * @param params     an object array contains the params
     * @return
     * @throws XmlRpcException
     */
    public Object execute(String namespace, String methodName, Object[] params) throws XmlRpcException {
        return client.execute(namespace.concat(DOT).concat(methodName), params);
    }
}
